﻿using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;

namespace RuntimeTypeGenerator
{
    /// <summary>
    /// Defines a runtime generated assembly that allows adding type using the <see cref="ITypeBuilder"/> interface.
    /// </summary>
    public class GeneratedAssembly : ITypeBuildTarget
    {
        private readonly Dictionary<string, Type> m_BuiltTypes = new Dictionary<string, Type>();

        private readonly AssemblyBuilder m_AssemblyBuilder;
        private readonly ModuleBuilder m_ModuleBuilder;
        private readonly AssemblyName m_AssemblyName;
        private readonly string m_AssemblyFileName;

        /// <summary>
        /// Gets the CLR assembly types are built to.
        /// </summary>
        public Assembly Assembly
        {
            get
            {
                return m_AssemblyBuilder;
            }
        }

        /// <summary>
        /// Defines a runtime generated assembly that allows adding type using the <see cref="ITypeBuilder"/> interface.
        /// </summary>
        /// <param name="name">The generated assembly name.</param>
        public GeneratedAssembly(string name)
            : this(name, null, null)
        {
        }

        /// <summary>
        /// Defines a runtime generated assembly that allows adding type using the <see cref="ITypeBuilder"/> interface.
        /// </summary>
        /// <param name="name">The generated assembly name.</param>
        /// <param name="version">The generated assembly version.</param>
        public GeneratedAssembly(string name, Version version)
            : this(name, version, null)
        {
        }

        /// <summary>
        /// Defines a runtime generated assembly that allows adding type using the <see cref="ITypeBuilder"/> interface.
        /// </summary>
        /// <param name="name">The generated assembly name.</param>
        /// <param name="version">The generated assembly version.</param>
        /// <param name="targetDirectory">The generated assembly target directory.</param>
        public GeneratedAssembly(string name, Version version, string targetDirectory)
        {
            if (string.IsNullOrEmpty(name))
            {
                throw new ArgumentNullException("name");
            }

            m_AssemblyName = new AssemblyName
            { 
                Name = name, 
                Version = version ?? new Version(1, 0), 
                ProcessorArchitecture = ProcessorArchitecture.MSIL 
            };
            m_AssemblyFileName = name + ".dll";

            m_AssemblyBuilder = System.Threading.Thread.GetDomain().DefineDynamicAssembly(m_AssemblyName, AssemblyBuilderAccess.RunAndSave, targetDirectory);
            m_ModuleBuilder = m_AssemblyBuilder.DefineDynamicModule(m_AssemblyFileName, m_AssemblyFileName);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="builder"></param>
        /// <returns></returns>
        public Type BuildWith(ITypeBuilder builder)
        {
            if (builder == null)
            {
                throw new ArgumentNullException("builder");
            }

            lock (m_BuiltTypes)
            {
                Type result = null;
                if (!m_BuiltTypes.TryGetValue(builder.TypeName, out result))
                {
                    try
                    {
                        result = builder.BuildType(m_ModuleBuilder);
                        m_BuiltTypes.Add(builder.TypeName, result);
                    }
                    catch (Exception exp)
                    {
                        throw new ArgumentException("Failed to build the type.", "builder", exp);
                    }
                }
                return result;
            }
        }

        /// <summary>
        /// Saves the generated assembly to disk.
        /// </summary>
        public void Save()
        {
            m_AssemblyBuilder.Save(m_AssemblyFileName);
        }
    }
}
